package tiltmaze.generator;

import java.util.HashSet;
import java.util.Random;
import tiltmaze.Cell;
import tiltmaze.Maze;
import tiltmaze.RowCol;
import tiltmaze.solver.MazeSolver;
import tiltmaze.solver.MazeStatesAnalyzer;

/**
 * Class used for generating mazes.
 * @author Juraj Nižnan
 */
public class MazeGenerator {

    private int dimRows;
    private int dimCols;
    private int dimCheckpoints;
    private int numOfWallPos;
    private Maze maze;
    private int timeBound = 0; //in seconds; 0 means unlimited
    private int possibleStates = 0;
    private int fitNum = 0;

    /**
     * Constructor. Random initialization.
     * @param dimRows
     * @param dimCols
     * @param dimCheckpoints
     */
    public MazeGenerator(int dimRows, int dimCols, int dimCheckpoints) {
        this.dimRows = dimRows;
        this.dimCols = dimCols;
        this.dimCheckpoints = dimCheckpoints;
        this.numOfWallPos = 2 * dimCols * dimRows - dimCols - dimRows;
        this.possibleStates = dimRows * dimCols * exp(dimCheckpoints);
        randomInit();
    }

    public MazeGenerator(Maze initMaze) {
        this.dimRows = initMaze.getNumOfRows();
        this.dimCols = initMaze.getNumOfCols();
        this.dimCheckpoints = initMaze.getNumOfCheckpoints();
        this.numOfWallPos = 2 * dimCols * dimRows - dimCols - dimRows;
        this.maze = initMaze;
        this.possibleStates = dimRows * dimCols * exp(dimCheckpoints);
    }

    /**
     * Sets maximum time in seconds allowed for generating the maze.
     * @param seconds
     */
    public void setTimeBound(int seconds) {
        this.timeBound = seconds;
    }

    /**
     * Selects fitness function. Can have value 0, 1 or 2.
     * @param num 0 or 1 or 2
     */
    public void setFit(int num) {
        //TODO restrict
        fitNum = num;
    }

    /**
     * Method that randomly initiates a maze.
     */
    private void randomInit() {
        this.maze = new Maze(dimRows, dimCols);
        Cell[][] cells = maze.getCells();
        HashSet<RowCol> unusedPositions = new HashSet<RowCol>();
        for (int i = 0; i < dimRows; i++) {
            for (int j = 0; j < dimCols; j++) {
                unusedPositions.add(new RowCol(i, j));
            }
        }
        Random random = new Random();
        //randomly place start:
        RowCol start = (RowCol) unusedPositions.toArray()[random.nextInt(unusedPositions.size())];
        unusedPositions.remove(start);
        cells[0][0].setStart(false);
        cells[start.row][start.col].setStart(true);
        //randomly place checkpoints:
        for (int i = 0; i < dimCheckpoints; i++) {
            RowCol chp = (RowCol) unusedPositions.toArray()[random.nextInt(unusedPositions.size())];
            unusedPositions.remove(chp);
            cells[chp.row][chp.col].setCheckpoint(true);
        }
        maze = new Maze(cells);
        //randomly place walls (each wall has 1/6 chance of existing):
        for (int i = 0; i < numOfWallPos; i++) {
            if (random.nextInt(6) == 0) {
                maze = MazeGenMoves.switchWall(maze, i);
            }
        }
    }

    /**
     * Local search heuristic for generating the maze.
     * @return
     */
    public Maze localSearch() {
        Maze lastMaze = null;
        Maze newMaze = maze;
        long startTime = System.currentTimeMillis();
        boolean timeStop = false;
        while (!newMaze.equals(lastMaze) && !timeStop) {
            lastMaze = newMaze;
            newMaze = searchNeighbourhood(lastMaze);
            MazeSolver solver = new MazeSolver(newMaze);
            solver.solve();
            // System.out.println(solver.getFit());
            timeStop = (timeBound == 0) ? false : ((System.currentTimeMillis() - startTime) > (timeBound * 1000));
        }
        return newMaze;
    }

    private Maze searchNeighbourhood(Maze m) {
        Maze bestMaze = m;
        double bestFit = getFit(bestMaze);
        Maze currMaze = m;
        double currFit;
        String op = "";
        String bop = "";
        RowCol[] checkpoints = findCheckpoints(m);
        HashSet<Integer> moves = new HashSet<Integer>();
        for (Integer i = 0; i < numOfWallPos + 4 * (dimCheckpoints + 1); i++) {
            moves.add(i);
        }
        Random random = new Random();
        while (moves.size() > 0) {
            Integer move = (Integer) moves.toArray()[random.nextInt(moves.size())];
            moves.remove(move);
            if (0 <= move && move < numOfWallPos) {
                //switch Wall:
                currMaze = MazeGenMoves.switchWall(m, move);
                op = "switch Wall " + move;
            }
            move -= numOfWallPos;
            if (0 <= move && move < 4) {
                //move start:
                currMaze = MazeGenMoves.moveStart(m, move);
                op = "move start " + move;
            }
            move -= 4;
            if (0 <= move) {
                //move checkpoint:
                currMaze = MazeGenMoves.moveCheckpoint(m, checkpoints[move / 4], move % 4);
                op = "move chpoint " + move / 4 + "in " + move % 4;
            }
            currFit = getFit(currMaze);
            if (currFit > bestFit) {
                bestFit = currFit;
                bestMaze = currMaze;
                bop = op;
            }
        }
        if (bestMaze.equals(m)) {
            //System.out.println("NO CHANGE !!");
        } else {
            //System.out.println(bestFit + " " + bop);
            //System.out.println(bestMaze);
        }
        return bestMaze;
    }

    private RowCol[] findCheckpoints(Maze m) {
        RowCol[] checkpoints = new RowCol[dimCheckpoints];
        int chpIndex = 0;
        Cell[][] cells = m.getCells();
        for (int i = 0; i < dimRows; i++) {
            for (int j = 0; j < dimCols; j++) {
                if (cells[i][j].getCheckpoint()) {
                    RowCol rc = new RowCol(i, j);
                    checkpoints[chpIndex] = rc;
                    chpIndex++;
                }
            }
        }
        return checkpoints;
    }

    private double getFit(Maze newMaze) {
        return getFit(newMaze, fitNum);
    }

    private double getFit(Maze maze, int num) {
        if (num<0 || num>2) {
            num=0;
        }
        double ret = 0;
        if (num == 0) {
            MazeStatesAnalyzer msa = new MazeStatesAnalyzer(maze);
            msa.analyze();
            ret = msa.getEdgeDeadlocks();
            ret = (ret == 1) ? 0 : ret;
            ret += (double) msa.getNodeCounter() / (double) possibleStates / 100.0;
            ret *= msa.getShortestSolutionLength();
        }
        if (num == 1) {
            //Shortest length+0,1*longest length
            MazeSolver ms = new MazeSolver(maze);
            ms.solve();
            ret = ms.getShortestSolutionLength() + 0.1 * ms.getLongestPathLength();
        }
        if (num == 2) {
            MazeStatesAnalyzer msa = new MazeStatesAnalyzer(maze);
            msa.analyze();
            ret = msa.getEdgeCounter();
        }
        return ret;
    }

    /**
     * Returns the generated maze.
     * @return
     */
    public Maze getMaze() {
        return maze;
    }

    /**
     * Executes one step of generation.
     */
    public void step() {
        maze = searchNeighbourhood(maze);
    }

    /**
     * @param a
     * @return 2^a
     */
    public static int exp(int a) {
        int ret = 1;
        for (int i = 0; i < a; i++) {
            ret = ret * 2;
        }
        return ret;
    }

    /**
     * Randomly perturbs the maze while preserving the dimensions and number of squares.
     * @param m
     * @return
     */
    public static Maze randomize(Maze m) {
        MazeGenerator mg = new MazeGenerator(m.getNumOfRows(), m.getNumOfCols(), m.getNumOfCheckpoints());
        return mg.getMaze();
    }
}
